import type { ClassValue } from "clsx";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { stringify as uuidStringify } from "uuid";
import { urlSafeBase64Decode } from "trailbase";

import { showToast } from "@/components/ui/toast";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export function pathJoin(parts: string[], sep?: string): string {
  const separator = sep ?? "/";
  const replace = new RegExp(`${separator}{1,}`, "g");
  return parts.join(separator).replace(replace, separator);
}

export function copyToClipboard(contents: string) {
  navigator.clipboard.writeText(contents);
  showToast({
    title: "Copied to clipboard",
  });
}

export function tryParseInt(value: string): number | undefined {
  const n = parseInt(value.trim());
  return isNaN(n) ? undefined : n;
}

export function tryParseBigInt(value: string): bigint | undefined {
  if (value === "") {
    return undefined;
  }

  try {
    return BigInt(value.trim());
  } catch {
    return undefined;
  }
}

export function tryParseFloat(value: string): number | undefined {
  const n = parseFloat(value.trim());
  return isNaN(n) ? undefined : n;
}

export function urlSafeBase64ToUuid(id: string): string {
  return uuidStringify(urlSafeBase64Decode(id));
}

export function toHex(bytes: Uint8Array): string {
  return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
}

export function fromHex(hex: string): Uint8Array {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
  }
  return bytes;
}

export async function showSaveFileDialog(opts: {
  contents: () => Promise<ReadableStream<Uint8Array> | null>;
  filename: string;
  mimeType?: string;
}): Promise<boolean> {
  const stream = await opts.contents();
  if (stream === null) {
    return false;
  }

  // Not supported by firefox: https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker#browser_compatibility
  // possible fallback: https://stackoverflow.com/a/67806663
  if (window.showSaveFilePicker) {
    try {
      const handle = await window.showSaveFilePicker({
        suggestedName: opts.filename,
      });

      const writable = await handle.createWritable();
      await stream.pipeTo(writable);
    } catch (err) {
      // Ignore user abortions.
      if (err instanceof Error && err.name === "AbortError") {
        return false;
      }
      throw err;
    }
  } else {
    const blob = await readableStreamToBlob(stream, opts.mimeType);

    const saveFile = document.createElement("a");
    saveFile.href = URL.createObjectURL(blob);
    saveFile.download = opts.filename;
    saveFile.click();

    // Cleanup.
    setTimeout(() => {
      saveFile.remove();
      URL.revokeObjectURL(saveFile.href);
    }, 60 * 1000);
  }

  return true;
}

async function readableStreamToBlob(
  stream: ReadableStream<Uint8Array>,
  mimeType?: string,
) {
  const reader = stream.getReader();

  const chunks: Uint8Array[] = [];
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      break;
    }
    chunks.push(value);
  }

  // Concatenate all chunks
  const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
  const merged = new Uint8Array(totalLength);
  let offset = 0;
  for (const chunk of chunks) {
    merged.set(chunk, offset);
    offset += chunk.length;
  }

  return new Blob([merged], { type: mimeType });
}

export function stringToReadableStream(s: string): ReadableStream {
  return new ReadableStream({
    start(controller) {
      controller.enqueue(new TextEncoder().encode(s));
      controller.close();
    },
  });
}
